Appearance
循环引用处理的区别
本节目标:理解“循环引用处理的区别”的核心思路,并能把它用于实际开发或面试表达。 太好了,你想深入具体细节,这些确实是 CommonJS 和 ESM 的核心区别。下面我分为三块分别说明:
一、Module Cache 模块缓存机制1. CommonJS:require.cache
- require 加载模块后,会把它的 module.exports 缓存在 require.cache 中。
- 第二次 require 同一个模块时,不再重新执行,只返回缓存的 exports。
- 缓存的是模块执行后的结果。
流程图(简化): require('a') → 查 require.cache → 无 → 读取 a.js → 执行模块(包一层函数)→ module.exports 放入 require.cache → 返回 exports 2. ESM:Module Record 缓存
- ESM 的缓存由 V8 实现,基于 Module Record。
- 缓存的是一个包含所有 export 的模块命名空间对象,即 ModuleNamespaceObject。
- 模块执行只发生一次,后续 import 都复用同一份命名空间对象。
二、循环引用处理(circular dependency)1. CommonJS 的循环引用
- 模块在第一次执行还没完成前就被引用,module.exports 会是一个半成品。
- 后续模块访问的是当前 exports 的初始状态,不是最终完整导出。
示例: // a.jsconst b = require('./b');console.log('a.js', b);module.exports = { fromA: 'A' }; // b.jsconst a = require('./a');console.log('b.js', a);module.exports = { fromB: 'B' }; 输出: b.js {}a.js { fromB: 'B' }
- 说明:b.js 引用了 a.js,但 a.js 还没执行完,因此拿到的是空对象 {}。
2. ESM 的循环引用
- 由于 ESM 是静态分析并建立 Module Graph(模块图),所以可以提前知道循环依赖。
- ESM 在模块初始化阶段就设置好 export 的引用,即使模块尚未执行完,其他模块也能访问并实时更新(Live Binding)。
示例: // a.mjsimport { valueFromB } from './b.mjs';console.log('a.mjs', valueFromB);export const valueFromA = 'A'; // b.mjsimport { valueFromA } from './a.mjs';console.log('b.mjs', valueFromA);export const valueFromB = 'B'; 输出: b.mjs undefineda.mjs B 说明:虽然 b.mjs 访问了 a.mjs 的变量,但是实时绑定的,值是 undefined 而非空对象。
三、V8 中的 Module 实现机制1. V8 中的 Module Record(模块记录)
- V8 对每个模块创建一个 Module Record 对象:
- 包含模块路径、导入依赖、导出变量、命名空间对象等。
- 每个 import 是一个引用,不是值拷贝。
2. 实际结构包含以下核心组成:
| 字段 | 描述 |
| [[ModuleNamespace]] | 代表该模块导出的命名空间对象 |
| [[Status]] | 当前状态:uninstantiated → instantiating → instantiated → evaluated |
| [[Dependencies]] | 导入的其它模块 |
| [[Environment]] | 执行环境,类似作用域 |
3. ESM 加载过程分为两阶段:① 实例化阶段(Instantiation Phase):
- 扫描 import/export
- 建立依赖图(Module Graph)
- 为导出变量分配空间(live binding)
② 执行阶段(Evaluation Phase):
- 按依赖顺序执行模块代码
- 支持异步 top-level await
V8 如何处理循环依赖?
- V8 在执行前先实例化所有模块,建立所有导出变量的引用(指针)。
- 哪怕值还没赋好,其他模块访问时也会拿到“引用”,并随着初始化完成而更新。
总结:关键差异对照
| 特性 | CommonJS | ESM |
| 缓存机制 | require.cache | V8 Module Record |
| 缓存内容 | 执行结果的 module.exports | 命名空间对象(含 live binding) |
| 包装方式 | 包装函数 | 原生支持,无需包装 |
| 循环引用支持 | 有限支持,引用时值未必完整 | 完整支持,引用是 live binding |
| 状态跟踪 | 无显式状态 | 由 V8 维护 Module 状态(状态机) |
| 实现粒度 | 脚本运行时实现 | 编译+运行期分离,便于优化和静态分析 |
如果你对 Node.js 的源码(如 module.js)、V8 的 SourceTextModule 实现或想了解如何模拟一个 module loader(自己写 require/import 系统)我也可以进一步展开。是否想看某段具体源码或模拟实现? > 来自 <https://chatgpt.com/c/6805aef6-e7b4-8004-ab43-9a137f666985>
